#!/usr/bin/python3
#-*- coding: utf-8 -*-

import asyncio
import hexdump
import ssl
import socket
import struct
import sys
from concurrent.futures import ThreadPoolExecutor

#openssl genrsa -out cert.pem 2048 
#openssl req -new -x509 -key key.pem -out cacert.pem -days 1095

default_key_pem = '''
-----BEGIN RSA PRIVATE KEY-----
MIIEpQIBAAKCAQEAqftbkvoowKFI8iFRSQpsJtML747nvRiX8z41D5tRbuDLnthd
VMj/9nMTdPv5WROC5LqU2JBS8M9iwTiCjhhLNGvYaKOUyaAWwgDjGVJBsR5YS31v
vUcWRNK8JImM+LyEl2Y6bxMdfIE5AEov4gBo6QUHeExWw0qTZbF4PSnJ7EcN1ACo
xotJDUUZ2T9dzPNB47k9u3B/Qak/S4iLTQXmuKa2hTt46xo0tnu3mrJAMpQZ7uOb
F6N9e5eO8+fxEvegEkd7X33rIAq21n5exas8Vka/OOJ+WK9+bxG9FVwPWKxG2LY+
KMdjW/NfgE36kkps4qEYYD21EfXLArebOp+T5wIDAQABAoIBAQCQBZEJbzuoOv97
aA9fO513J8otJ3/iEDxutH5wwsC0mXLtG5W4iLWXMtmBfR1AA/AbrJsBAwfDUXPz
EtZLXvoZaGbsAe3HCAVAS1Af3DocwAE/tQA/90M+/O68Zm8zMUeGn+9SvXd2Tmbl
f/YiHwbEqKYznzrELSPGPiAKcqXLsuam7XvDichSmh/eozOhLsC7q0VouTEs0LeA
57M/M/b6Z2j882ch1nLMmHNqHfB+UDbDySC+dPE7/J3ldhSLNfiIpq8XVySqTaQJ
nlD11gsC672rfI9MbOu4lxcPOUACssK/AP4FNQDMOKarGufkE7Npny0P8LaDR+cz
cz8UCrZBAoGBANf+PG/7I0wXRqesQZ6/0c4RHOaQRS0LztH5BpV0vuZykO98xK9o
ZuOoHbDvoNRYyyV2b510ZLVbfqoIltSi49TH8TqSPGAHsnkHDzGtYFrJMolA/mFH
Yh/wExplitt6GIhAjPKJ2hag+2v76sveqabcGZ+PQDUYZrGufXLyejw7AoGBAMl3
Zwt2JUmCIqcsDhMfac6BTb1G/CPSVgldIihqXyrX7NKOnx4YKJ4puA5BD+WnQMMa
g66G7iiRe7rmk9vZ2BnuBi7P9OIq4sdr6j2P6AhYyR2hK6cAuP2xB+5zZw8UYChw
PMhwVKcybHL3DBbq0wJzd7FAFweSl0Z+johKKohFAoGAATWkQwBGB1QOCjxVtbzf
QOcpoUTnjkQFqXlahxW7R/EWTLEHLPBJuzaiy+3bmbTRlmkeYORY+AcGI4+ewJwU
6IkiGbDd86+3WSQn+qA87OU5ijk583OB4innAXvWFjqNzp2F7InHFd9a6RTlELUz
gAK1hgY4VtPHJ3GRtEYVVycCgYEAtT0qQSBW6OseW3+BZeiTmceu9MDAIUWeSqwz
B2KyHGvFS6IsOLmJCMgFISWctH+i/iGDJuWRqINAaOH3UAeIpcMAYuqHpVLpUned
cSZdmeyteG8+Qu2effLdbMD6jJFK8/YMaFDNhuAQudfPU2qKyyCvI5DBZvmUB0Hg
3XDDFHUCgYEAkbblYr7weBlRjWQiPtiSJ/ZZY7mnDlTHsYRqm9Vu1CwQC0ozJRwL
eMJ5u1dbZf+fQVuVcAfNFQC3AkN+CSU/TIg3T35+FqhDLiQ6Dy/2HkI9QZGkOmLr
yN3UVM17D/qmIQ23FImQ2lyOUNOsi9a8IWr7+c6t6DQ0kxK3el194ek=
-----END RSA PRIVATE KEY-----
'''

default_cert_pem = '''
-----BEGIN CERTIFICATE-----
MIIDkzCCAnugAwIBAgIULX+hrliqTU33fL3yBdRJrWY41CEwDQYJKoZIhvcNAQEL
BQAwWTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDESMBAGA1UEAwwJcHB5X3Byb3h5MB4X
DTIxMDIyODEzMjEzOVoXDTI0MDIyODEzMjEzOVowWTELMAkGA1UEBhMCQVUxEzAR
BgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5
IEx0ZDESMBAGA1UEAwwJcHB5X3Byb3h5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
MIIBCgKCAQEAqftbkvoowKFI8iFRSQpsJtML747nvRiX8z41D5tRbuDLnthdVMj/
9nMTdPv5WROC5LqU2JBS8M9iwTiCjhhLNGvYaKOUyaAWwgDjGVJBsR5YS31vvUcW
RNK8JImM+LyEl2Y6bxMdfIE5AEov4gBo6QUHeExWw0qTZbF4PSnJ7EcN1ACoxotJ
DUUZ2T9dzPNB47k9u3B/Qak/S4iLTQXmuKa2hTt46xo0tnu3mrJAMpQZ7uObF6N9
e5eO8+fxEvegEkd7X33rIAq21n5exas8Vka/OOJ+WK9+bxG9FVwPWKxG2LY+KMdj
W/NfgE36kkps4qEYYD21EfXLArebOp+T5wIDAQABo1MwUTAdBgNVHQ4EFgQUw5+Q
bRz+flYRz5Idr9wKZ7N13LkwHwYDVR0jBBgwFoAUw5+QbRz+flYRz5Idr9wKZ7N1
3LkwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAHy5ORQ7PVmQH
jIEl1s80R5tXkj8w30faaRhPB0q5Uaid0cWa1NTvpYE0lXydkVvJfBMMR6wgJlKi
pXZLvrmraPFs5uNa1UH97PBJQVQKOqeDXN+PNNjFdB8zMAaCfqyWNGS7fWV+59Pj
cDTLa4w2w/GxyRlHq2A65VlVV0n8KX1p0h9xC1UR9hpUTdRU9vwqceEh/sehwIKI
nz417ltcFH+C1MsHoCKCopVk5awag8wx9+iN4KiVnYKOY0WEr4kAlSH3FGbv8JeN
eFFQv/knLggsxDNL/QrhUCkztQWCT+DTDoQiiOZHjTYTzgauk942OC7USIXvPxY/
5LDVbTAxHg==
-----END CERTIFICATE-----
'''
def write_file(fileName, data):
    with open(fileName, 'w', encoding='utf-8') as f:
        f.write(data)

class tcpproxy_config:
    def __init__(self):
        self.local_ip    = '0.0.0.0'
        self.local_port  = 7066
        self.remote_ip   = ''
        self.remote_port = 0
        self.target_ip   = '' 
        self.target_port = 0
        self.listen_type = 'socks5' #tcp, socks5
        self.next_type   = 'tcp'    #tcp, http_connect
        self.listen_ssl  = True
        self.next_ssl    = False

class tcpproxy:
    def __init__(self, json_cfg):
        self.__executor = ThreadPoolExecutor(3)
        self.__loop = asyncio.get_event_loop()

        self.__json_cfg = json_cfg
        
        self.__a_reader = None
        self.__a_writer = None
        
        self.__c_reader = None
        self.__c_writer = None
        
    async def __copy_data_thread(self, copy_type):
        addr_a = self.__a_writer.get_extra_info('peername')
        addr_c = self.__c_writer.get_extra_info('peername')

        addr_str = "[A->P->B]{0}:{1}->{2}:{3}".format(addr_a[0], addr_a[1], addr_c[0], addr_c[1])
        
        if copy_type == 1:
           r = self.__a_reader
           w = self.__c_writer
        else:
           r = self.__c_reader
           w = self.__a_writer
           addr_str = "[B->P->A][{0}:{1}->{2}:{3}]".format(addr_c[0], addr_c[1], addr_a[0], addr_a[1])
    
        while True:
            chunk = await r.read(1024)
            if not chunk:
                break
            print("{0}[{1}]".format(addr_str, len(chunk)))
            #hexdump.hexdump(chunk)
            w.write(chunk)
            await w.drain()

        print("close sock[{0}]:{1}".format(info, addr))
        writer.close()

    async def accept_handle(self, a_reader, a_writer):
        self.__a_reader = a_reader
        self.__a_writer = a_writer

        if self.__json_cfg.next_type == 'http_connect':
            isOK = await self.__connect_http()
            if isOK == False:
                return

        if self.__json_cfg.listen_type == 'socks5':
            isOK = await self.__connect_socks5()
            if isOK == False:
                return

        dltasks = set()
        dltasks.add(asyncio.ensure_future(self.__copy_data_thread(1)))
        dltasks.add(asyncio.ensure_future(self.__copy_data_thread(2)))
        dones, dltasks = await asyncio.wait(dltasks, return_when=asyncio.FIRST_COMPLETED)

    def __http_connect_handshake_python_v38_v39(self):
        json_cfg = self.__json_cfg
        
        w_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        w_sock.settimeout(10)
        w_sock.connect((json_cfg.remote_ip, json_cfg.remote_port))

        cmd_connect = "CONNECT {0}:{1} HTTP/1.1\r\n\r\n".format(json_cfg.target_ip, json_cfg.target_port)
        w_sock.sendall(str.encode(cmd_connect))
        
        res_str = ""
        while True:
            chunk = w_sock.recv(1024)
            if not chunk: 
                break
            
            res_str = res_str + bytes.decode(chunk)
            
            if bytes.decode(chunk).find("\r\n\r\n") > 0: 
                break

        print("<-- {0}".format(res_str))
        if res_str.lower().find("200 connection established") < 0:
            raise Exception("Unable to establish HTTP-Tunnel: {0}".format(res_str))
        
        return w_sock

    async def __http_connect_handshake_python_v37(self):
        json_cfg = self.__json_cfg
        r, w = await asyncio.open_connection(host=json_cfg.remote_ip, port=json_cfg.remote_port)

        cmd_connect = "CONNECT {0}:{1} HTTP/1.1\r\n\r\n".format(json_cfg.target_ip, json_cfg.target_port)
        
        w.write(str.encode(cmd_connect))
        await w.drain()
            
        res_str = ""
        while True:
            chunk = await r.read(1024)
            if not chunk: 
                break
            
            res_str = res_str + bytes.decode(chunk)
            
            if bytes.decode(chunk).find("\r\n\r\n") > 0: 
                break

        print("<-- {0}".format(res_str))
        if res_str.lower().find("200 connection established") < 0:
            raise Exception("Unable to establish HTTP-Tunnel: {0}".format(res_str))
        
        tr = w.transport
        w_sock = tr.get_extra_info('socket')

        return w_sock
        
    async def __connect_http(self):
        print("__connect_http")
        
        json_cfg = self.__json_cfg
        
        if sys.version_info >= (3, 7):
            w_sock = await self.__loop.run_in_executor(self.__executor, self.__http_connect_handshake_python_v38_v39)
            print('sys.version_info:http_connect_handshake_python_v38_v39')
        else:
            w_sock = await self.__http_connect_handshake_python_v37()
            print('sys.version_info:http_connect_handshake_python_v37')

        if json_cfg.next_ssl == True:
            print("start asyncio.open_connection(ssl):{0}:{1}".format(json_cfg.remote_ip, json_cfg.remote_port))
            ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
            ssl_context.check_hostname = False
#           ssl_context.load_cert_chain('cert.pem','key.pem')
            ssl_context.load_default_certs()

            self.__c_reader, self.__c_writer = await asyncio.open_connection(
                sock=w_sock,
                server_hostname=json_cfg.target_ip,
                ssl=ssl_context
                )
            print("end asyncio.open_connection(ssl):{0}:{1}".format(json_cfg.remote_ip, json_cfg.remote_port))
        else:
            try:
                print("start asyncio.open_connection(tcp):{0}:{1}".format(json_cfg.remote_ip, json_cfg.remote_port))
                self.__c_reader, self.__c_writer = await asyncio.open_connection(sock=w_sock)
                print("end asyncio.open_connection(tcp):{0}:{1}".format(json_cfg.remote_ip, json_cfg.remote_port))
            except socket.error as e1:  
                print('socket error_e1:{0}'.format(e1)) 
                a_writer.close()
                return

        return True
    
    async def __connect_socks5(self):
        print("__connect_socks5")
        
        reader   = self.__a_reader
        writer   = self.__a_writer
        json_cfg = self.__json_cfg
        
        try:
            ver = await reader.read(1)
            methods_len = await reader.read(1)
            methods = await reader.read(ord(methods_len))
        
            print("ver = {0}, methods = {1}".format(ver, methods))
        
            writer.write((b'\x05\x00'))
            await writer.drain()
        
            ver   = await reader.read(1)
            cmd   = await reader.read(1)
            rsv   = await reader.read(1)
            atype = await reader.read(1)

            print("ver = {0}, cmd = {1}, rsv = {2}, atype = {3}".format(ver, cmd, rsv, atype))

            if ord(cmd) != 1:
                print("ord(cmd) is not 1")
                writer.close()
                return False
            
            # 判断是否支持atype，目前不支持IPv6
            # 比特流转化成整型 big表示编码为大端法，
            if ord(atype) == 1:
            # IPv4
                json_cfg.remote_ip = socket.inet_ntoa(await reader.read(4))
                json_cfg.remote_port = struct.unpack(">H", await reader.read(2))[0]

                print("__remote_ip = {0}, __remote_port = {1}".format(json_cfg.remote_ip, json_cfg.remote_port))
            elif ord(atype) == 3:
                # 域名
                addr_len = ord(await reader.read(1))
                json_cfg.remote_ip = await reader.read(addr_len)
                json_cfg.remote_port = struct.unpack(">H", await reader.read(2))[0]
            
                print("__remote_ip = {0}, __remote_port = {1}".format(json_cfg.remote_ip, json_cfg.remote_port))
            else:
                # 不支持则关闭连接
                reply = b"\x05\x08\x00\x01" + socket.inet_aton("0.0.0.0") + struct.pack(">H", 2222)
                writer.write(reply)
                await writer.drain()
                writer.close()
                return False
            try:
                if json_cfg.next_ssl == True:
                    ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
                    ssl_context.check_hostname = False
#                    ssl_context.load_cert_chain('cert.pem','key.pem')
                    ssl_context.load_default_certs()
                    print("start asyncio.open_connection(ssl):{0}:{1}".format(json_cfg.remote_ip, json_cfg.remote_port))
                    self.__c_reader, self.__c_writer = await asyncio.open_connection(
                        json_cfg.remote_ip, 
                        json_cfg.remote_port, 
                        ssl=ssl_context, 
                        server_hostname=json_cfg.target_ip
                        )
                    print("end asyncio.open_connection(ssl):{0}:{1}".format(json_cfg.remote_ip, json_cfg.remote_port))
                else:
                    print("start asyncio.open_connection(tcp):{0}:{1}".format(json_cfg.remote_ip, json_cfg.remote_port))
                    self.__c_reader, self.__c_writer = await asyncio.open_connection(json_cfg.remote_ip, json_cfg.remote_port)
                    print("end asyncio.open_connection(tcp):{0}:{1}".format(json_cfg.remote_ip, json_cfg.remote_port))
            except socket.error as e1:  
                print('socket error_e1:{0}'.format(e1)) 
                reply = '\x05\x05\x00\x01\x00\x00\x00\x00\x00\x00'
                writer.write(reply)
                await writer.drain()
                writer.close()
                return False
            
            addr = self.__c_writer.get_extra_info('peername')
            #print(addr)
        
            reply = b"\x05\x00\x00\x01" + socket.inet_aton(addr[0]) + struct.pack(">H", addr[1])
            print("reply={0}".format(reply))
            writer.write(reply)
            await writer.drain()
            return True
        except socket.error as e2:  
            print('socket error_e2:{0}'.format(e2))  

        return False

def ppy_tcp_proxy(json_cfg):
    tp = tcpproxy(json_cfg)

    loop = asyncio.get_event_loop()
    coro = None
    
    ssl_context = None
    if json_cfg.listen_ssl == True:
        ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
        ssl_context.check_hostname = False
        ssl_context.load_cert_chain('cert.pem','key.pem')

    coro = asyncio.start_server(tp.accept_handle, json_cfg.local_ip, json_cfg.local_port, loop=loop, ssl=ssl_context)
    server = loop.run_until_complete(coro)

    print('Serving on {}'.format(server.sockets[0].getsockname()))
    try:
        loop.run_forever()
    except KeyboardInterrupt:
        pass

    server.close()
    loop.run_until_complete(server.wait_closed())
    loop.close()

def main_client():
    json_cfg = tcpproxy_config()
    
    json_cfg.local_ip    = '0.0.0.0'
    json_cfg.local_port  = 7000
    json_cfg.remote_ip   = '127.0.0.1'
    json_cfg.remote_port = 7100
    json_cfg.target_ip   = 'cwn1.f3322.net'
    json_cfg.target_port = 7066
    json_cfg.listen_type = 'tcp'          #tcp, socks5
    json_cfg.next_type   = 'http_connect' #tcp, http_connect
    json_cfg.listen_ssl  = False
    json_cfg.next_ssl    = True

    ppy_tcp_proxy(json_cfg)

def main_server():
    write_file('key.pem', default_key_pem)
    write_file('cert.pem', default_cert_pem)

    json_cfg = tcpproxy_config()
    
    json_cfg.local_ip    = '0.0.0.0'
    json_cfg.local_port  = 7066
    json_cfg.remote_ip   = ''
    json_cfg.remote_port = 0
    json_cfg.target_ip   = ''
    json_cfg.target_port = 0
    json_cfg.listen_type = 'socks5' #tcp, socks5
    json_cfg.next_type   = 'tcp'    #tcp, http_connect
    json_cfg.listen_ssl  = True
    json_cfg.next_ssl    = False
    
    ppy_tcp_proxy(json_cfg)

if __name__ == "__main__":
    main_client()
    #main_server()